package ca.scotthyndman.as2haxe.dom.binding;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ca.scotthyndman.as2haxe.dom.ASCompilationUnit;
import ca.scotthyndman.as2haxe.dom.ASFunction;
import ca.scotthyndman.as2haxe.dom.ASImportDeclaration;
import ca.scotthyndman.as2haxe.dom.ASName;
import ca.scotthyndman.as2haxe.dom.ASPackage;
import ca.scotthyndman.as2haxe.dom.ASPropertyDeclaration;
import ca.scotthyndman.as2haxe.dom.ASQualifiedName;
import ca.scotthyndman.as2haxe.dom.ASSimpleName;
import ca.scotthyndman.as2haxe.dom.ASTNode;
import ca.scotthyndman.as2haxe.dom.ASType;
import ca.scotthyndman.as2haxe.dom.ASTypeDeclaration;
import ca.scotthyndman.as2haxe.dom.ASVariableDeclaration;
import ca.scotthyndman.as2haxe.dom.IScopeElement;

/**
 * Represents the scope of an ASTNode.
 */
public class ASTScope {

	/** Caches scope information */
	private static final Map<ASTNode, ASTScope> scopeCache = new HashMap<ASTNode, ASTScope>();

	/** Holds the scope's node */
	private ASTNode node;

	/** Declaring type */
	private ASTypeDeclaration declaringType;

	/** Holds all types currently in scope */
	private Map<String, ASTypeDeclaration> typesInScope = new HashMap<String, ASTypeDeclaration>();

	/** Variable in scope */
	private Map<String, ASVariableDeclaration> variablesInScope = new HashMap<String, ASVariableDeclaration>();

	/** Properties in scope */
	private Map<String, ASType> propertiesInScope = new HashMap<String, ASType>();

	/** Functions in scope */
	private Map<String, ASFunction> functionsInScope = new HashMap<String, ASFunction>();

	/**
	 * Constructs a new node scope.
	 * 
	 * @param node
	 */
	private ASTScope(ASTNode node) {
		this.node = node;
		scopeCache.put(node, this);

		initWithDefaults();
	}

	/**
	 * Fills the node with top-level functions.
	 */
	private void initWithDefaults() {
		ASTypeDeclaration topLevel = getTypeDeclarationForName(new ASSimpleName(
				this.node.getAST(), "TopLevel"));

		fillScope(this, topLevel, true);
	}

	/**
	 * Gets the scope node.
	 * 
	 * @return the node
	 */
	public ASTNode getNode() {
		return node;
	}

	/**
	 * Adds everything from another scope into this one.
	 * 
	 * @param scope
	 */
	private void addToScope(ASTScope scope) {
		this.typesInScope.putAll(scope.typesInScope);
		this.functionsInScope.putAll(scope.functionsInScope);
		this.variablesInScope.putAll(scope.variablesInScope);
		this.declaringType = scope.declaringType;
	}

	/**
	 * Gets
	 * 
	 * @param name
	 * @return
	 */
	public ASTNode getNodeForName(String name) {
		String[] parts = name.split("\\.");
		ASTNode currentNode = null;
		ASTScope currentScope = this;
		ASTypeDeclaration currentScopeType = declaringType;

		for (int i = 0; i < parts.length; i++) {
			String part = parts[i];

			if (part.equals("this")) {
				currentNode = currentScope.declaringType;
				currentScopeType = currentScope.declaringType;
			} else if (part.equals("super")) {
				currentNode = currentScope.declaringType.getSuperClassType();
				
				if (currentNode == null) {
					break;
				}
				
				currentScopeType = currentScope
						.getTypeDeclarationForName(currentScope.declaringType
								.getSuperClassType().getName());
			} else if (this.variablesInScope.containsKey(part)) {
				currentNode = currentScope.variablesInScope.get(part);
				
				//
				// Break if nothing
				//
				if (((ASVariableDeclaration) currentNode).getType() == null) {
					break;
				}

				currentScopeType = currentScope
						.getTypeDeclarationForName(((ASVariableDeclaration) currentNode)
								.getType().getName());
			} else if (this.functionsInScope.containsKey(part)) {
				currentNode = currentScope.functionsInScope.get(part);

				//
				// Break if nothing
				//
				if (((ASFunction) currentNode).getReturnType() == null) {
					break;
				}

				currentScopeType = currentScope
						.getTypeDeclarationForName(((ASFunction) currentNode)
								.getReturnType().getName());
			} else if (this.propertiesInScope.containsKey(part)) {
				currentNode = currentScope.propertiesInScope.get(part);
				
				//
				// Break if nothing
				//
				if (((ASPropertyDeclaration) currentNode).getType() == null) {
					break;
				}
				
				currentScopeType = currentScope
						.getTypeDeclarationForName(((ASPropertyDeclaration) currentNode)
								.getType().getName());
			} else {
				
			}

			if (i + 1 < parts.length) {
				currentScope = ASTScope.getScope(currentScopeType);
				currentNode = null;
			}
		}

		return currentNode;
	}

	/**
	 * Returns the type declaration for <code>type</code> if it is in scope.
	 * 
	 * @param typeName
	 * @return the type declaration, or <code>null</code> if not found
	 */
	public ASTypeDeclaration getTypeDeclarationForName(ASName typeName) {
		ASSimpleName name = null;
		ASName quali = null;
		if (typeName.isSimpleName()) {
			name = (ASSimpleName) typeName;
		} else {
			name = ((ASQualifiedName) typeName).getName();
			quali = ((ASQualifiedName) typeName).getQualifier();
		}

		//
		// If we have a qualifier, go direct
		//
		if (quali != null) {
			ASPackage pkg = ASPackage.getPackage(quali.toString());
			return pkg == null ? null : pkg.getTypesMap().get(name.toString());
		}

		//
		// Lookup the type
		//
		if (typesInScope.containsKey(name.toString())) {
			return typesInScope.get(name.toString());
		}

		//
		// Check root package
		//
		if (ASPackage.getRootPackage().getTypesMap().containsKey(name.toString())) {
			return ASPackage.getRootPackage().getTypesMap().get(name.toString());
		}

		return null;
	}

	/**
	 * Builds the scope of the node.
	 */
	private static ASTScope buildScopeTree(ASTNode node) {
		if (scopeCache.containsKey(node)) {
			return scopeCache.get(node);
		}

		ASTScope scope = new ASTScope(node);
		ASTScope parentScope = null;

		//
		// Fill scope
		//
		if (node instanceof ASTypeDeclaration) {
			fillScope(scope, (ASTypeDeclaration) node);
		}

		//
		// Block scope
		//
		if (node instanceof IScopeElement) {
			fillScope(scope, (IScopeElement) node, true);
		}

		//
		// Deal with parent scopes
		//
		if (!(node.getParent() instanceof ASPackage)) {
			parentScope = buildScopeTree(node.getParent());
		}

		if (parentScope != null) {
			scope.addToScope(parentScope);
		}

		//
		// Returns the scope
		//
		return scope;
	}

	/**
	 * Fills a scoped element scope.
	 * 
	 * @param scope
	 *            the scope to fill
	 * @param element
	 *            the scope element
	 * @param overwrite
	 *            <code>true</code> if existing values should be overwritten
	 */
	private static void fillScope(ASTScope scope, IScopeElement element,
			boolean overwrite) {
		for (ASVariableDeclaration var : element.getVariablesInScope()) {
			String key = var.getName().toString();
			if (overwrite || !scope.variablesInScope.containsKey(key)) {
				scope.variablesInScope.put(key, var);
			}
		}

		for (ASFunction func : element.getFunctionsInScope()) {
			String key = func.getName().toString();
			if (func.isProperty()) {
				if (func.isGetter()) {
					if (overwrite || !scope.propertiesInScope.containsKey(key)) {
						scope.propertiesInScope.put(key, func.getReturnType());
					}
				}
				continue;
			} else if (func.getName() == null) {
				// TODO figure out local functions
				continue;
			}

			if (overwrite || !scope.functionsInScope.containsKey(key)) {
				scope.functionsInScope.put(key, func);
			}
		}
	}

	/**
	 * Fills a type scope.
	 * 
	 * @param type
	 * @return
	 */
	private static void fillScope(ASTScope scope, ASTypeDeclaration type) {
		List<ASTNode> imports = ((ASCompilationUnit) type.getParent()).getImports();
		for (ASTNode imp : imports) {
			ASQualifiedName name = ((ASImportDeclaration) imp).getName();

			//
			// add everything from package or just add a type
			//
			if (name.getName().toString().equals("*")) {
				ASPackage pkg = ASPackage.getOrCreatePackage(name
						.getQualifier());
				for (ASTypeDeclaration pkgType : pkg.getTypesMap().values()) {
					scope.typesInScope.put(pkgType
							.getName().getName().toString(), pkgType);
				}
			} else {
				ASPackage pkg;
				if (name.getQualifier() == null) {
					pkg = ASPackage.getRootPackage();
				} else {
					pkg = ASPackage.getOrCreatePackage(name.getQualifier());
				}

				ASTypeDeclaration pkgType = pkg.getTypesMap().get(
						name.getName().toString());
				if (pkgType != null) {
					scope.typesInScope.put(pkgType
							.getName().getName().toString(), pkgType);
				}
			}
		}

		scope.typesInScope.put(type.getName().getName().toString(), type);
		scope.declaringType = type;

		//
		// Pull in super type stuff
		//
		if (type.getSuperClassType() != null) {
			ASTypeDeclaration superType = scope.getTypeDeclarationForName(type
					.getSuperClassType().getName());
			if (superType != null) {
				fillScope(scope, superType, false);
			}
		}
	}

	/**
	 * Gets the scope for a particular node.
	 * 
	 * @param node
	 * @return
	 */
	public static ASTScope getScope(ASTNode node) {
		if (scopeCache.containsKey(node)) {
			return scopeCache.get(node);
		}

		return buildScopeTree(node);
	}
}
